Harden SwiftData store against data loss (demo wipe + schema versioning)#2
Open
hoveeman wants to merge 11 commits into
Open
Harden SwiftData store against data loss (demo wipe + schema versioning)#2hoveeman wants to merge 11 commits into
hoveeman wants to merge 11 commits into
Conversation
…cations Adds an "On-device (Apple)" provider that runs the AI coach entirely on the device via FoundationModels — no API key, no network, fully private. It falls back to a chosen cloud provider (OpenAI/Gemini/OpenRouter) when the local model is unavailable or a generation fails. Provider: - AppleFoundationModelsClient: a ResponsesClient adapter (one-shot, tool-less in v1) using FoundationModels guided generation for structured cards, with a text/JSON fallback. Mirrors the GeminiClient request-body translation. - AppleOnDeviceAvailability: friendly wrapper over SystemLanguageModel availability; guarded with #if canImport(FoundationModels) / @available so the project still compiles on older SDKs. - FallbackResponsesClient + CoachClientResolver: unify provider resolution across chat/summaries/notifications and add the on-device → cloud-backup chain. - Settings: On-device provider option, cloud-backup picker, privacy card. Notifications (leveraging free/private local inference): - Richer check-ins: actionable tip + tappable follow-up + adaptive skip. - New midday slot alongside morning/evening. - Proactive anomaly alerts (low SpO2, short sleep), event-driven and on-device-only, deduped per kind per day. - On-device check-ins use a BGProcessingTask (longer budget, no network required); cloud providers keep BGAppRefreshTask. Adds the com.pulseloop.coach.process background-task identifier. - Fixes a latent crash: background scheduling now gates on actual BGTask registration (the host app could otherwise submit an unregistered identifier under XCTest). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent ways a build could destroy real user data are removed or made recoverable: - RootViews launch-time `clearAll()`+`seedDemo()` was gated only by a `seedDemo` UserDefaults bool, so it ran in any configuration. Wrap it in `#if DEBUG` — archived Release/TestFlight builds can no longer wipe the store from a stray default. - The Privacy & Data "Clear demo data" / "Reseed demo data" buttons both call `clearAll()` (deletes every row of every model). Gate them behind `#if DEBUG` so the only in-app full-wipe path is gone from shipping UI. - ModelContainerFactory replaced its destructive failure mode: on a failed inferred migration it now moves the existing store (and -wal/-shm sidecars) to a timestamped backup and recreates, instead of crash-looping or silently dropping data. Additive schema changes still migrate automatically; non-additive ones preserve the old store for recovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt VersionedSchema/SchemaMigrationPlan so schema changes between TestFlight builds run explicit, reviewable migrations instead of SwiftData's implicit inference (which fails silently on non-additive changes and risks wiping real user data). - PulseLoopSchemaV1 baselines the 23 current @model types as version 1.0.0. Existing on-disk stores match this shape exactly, so they're recognised as already-at-V1 — no migration runs, no data is touched. - PulseLoopMigrationPlan lists V1 with no stages yet; doc comments spell out the per-change procedure for adding V2 (+ lightweight/custom stage). - ModelContainerFactory builds against the versioned schema and passes the plan. The backup-then-recreate fallback remains as the last-resort net for an unexpected/unmigratable store. Verified: Debug + Release simulator builds succeed; all 168 unit tests pass (they create the container in-memory through this factory). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…imodal-images Add image input to the AI Coach (multimodal)
…-for-upstream Add Apple on-device (FoundationModels) coach provider + rich notifications
# Conflicts: # PulseLoop/Info.plist
The background-task guard in flushNow() (commit ed608a5) references UIApplication, but its matching import lived only in uncommitted local work. Restore the canImport(UIKit) guard so the branch compiles standalone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A TestFlight tester (the app owner) lost real data, repeatedly replaced by the upstream demo seed (Saksham / Pittsburgh workouts). Root cause was not an automatic seed in production — it traced to running a
-seedDemodev build against the same device/bundle id that holds real data (which wrote demo rows into the persistent store), plus the all-or-nothing "Clear demo data" button. Because TestFlight builds are archived from an uncommitted working tree, every build's safety depended on that tree's state.This makes data loss structurally impossible in Release builds, and adds explicit schema versioning so future model changes migrate safely instead of risking the store.
Changes
Close the wipe paths (Release-safe)
RootViews— launch-timeclearAll()+seedDemo()was gated only by aseedDemoUserDefaultsbool, so it could run in any configuration. Now#if DEBUG. Archived builds can no longer wipe the store from a stray default.PrivacyDataSettingsView— "Clear demo data" / "Reseed demo data" both callSeedData.clearAll()(deletes every row of every model). Gated behind#if DEBUG, removing the only in-app full-wipe path from shipping UI.Explicit schema versioning + safe failure
PulseLoopSchemaV1baselines the 23 current@Modeltypes as version1.0.0. Existing on-disk stores match this shape exactly, so they're recognised as already-at-V1 — no migration runs, no data is touched.PulseLoopMigrationPlanlists V1 with no stages yet; doc comments spell out the per-change procedure for introducing V2 (+ a lightweight or custom stage). This replaces SwiftData's implicit inference, which fails silently on non-additive changes.ModelContainerFactorybuilds against the versioned schema and passes the plan. On an unexpected/unmigratable store it moves the store (and-wal/-shmsidecars) to a timestamped backup and recreates — neverfatalErrorcrash-loop, never silent data drop.How to add the next schema version
When you change a model, follow the steps in the
PulseLoopSchemaV1doc comment: addPulseLoopSchemaV2, append aMigrationStage(.lightweightfor additive/renames,.customfor transforms), and pointcurrentSchemaat V2. Purely additive changes (new model, new optional property) need only the new version + a.lightweightstage.Testing
xcodebuild build— Debug and Release both succeed (signing-free simulator).xcodebuild test— all 168 unit tests pass; they create the container in-memory throughModelContainerFactory, exercising the migration plan at runtime.Note
The separate background crash (
MainActor.assumeIsolatedin the BGTask handler) was already fixed earlier on this branch (a0456f3) and is not part of this PR.🤖 Generated with Claude Code